{- This file is part of irc-fun-bot.
 -
 - Written in 2015, 2016 by fr33domlover <fr33domlover@riseup.net>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

-- For JSON field names
{-# LANGUAGE OverloadedStrings #-}

-- To allow 'Channel' as a JSON object key
{-# LANGUAGE FlexibleInstances #-}

module Network.IRC.Fun.Bot.Internal.Persist
    ( loadBotState
    , mkSaveBotState
    , saveBotState
    , selectChannel
    , unselectChannel
    , addChannelState
    )
where

import Control.Applicative
import Control.Monad (mzero, unless, when)
import Control.Monad.IO.Class (liftIO)
import Data.Aeson
import Data.JsonState
import Data.List (union)
import Data.Time.Interval
import Data.Time.Units (Microsecond)
import Network.IRC.Fun.Bot.Internal.Instances ()
import Network.IRC.Fun.Bot.Internal.IrcLog (makeLogger)
import Network.IRC.Fun.Bot.Internal.Monad
import Network.IRC.Fun.Bot.Internal.State
import Network.IRC.Fun.Bot.Internal.Types
import Network.IRC.Fun.Client.NickTracker (newNetwork)
import Network.IRC.Fun.Types (Channel (..))

import qualified Data.HashMap.Lazy as M
import qualified Data.HashSet as S

data BotStateJ = BotStateJ (M.HashMap Channel ChanInfo) (S.HashSet Channel)

toJ :: BotState s -> BotStateJ
toJ bstate =
    BotStateJ (M.map stateToInfo $ bsChannels bstate) (bsSelChans bstate)

fromJ :: BotEnv e s -> BotStateJ -> s -> IO (BotState s)
fromJ env (BotStateJ stateJ selJ) pub = do
    let f chan (ChanInfo tracking counting logging hls defresp) = do
            mlogger <-
                if logging
                    then fmap Just $ makeLogger env chan
                    else return Nothing
            return $ ChanState tracking counting mlogger hls defresp
        defstate = ChanInfo False False False 0 True
        chansConf = S.toList selJ `union` cfgChannels (beConfig env)
        stateConf = M.fromList $ zip chansConf (repeat defstate)
        stateAll = stateJ `M.union` stateConf
    cstate <- M.traverseWithKey f stateAll
    return $ BotState newNetwork cstate S.empty selJ M.empty M.empty pub

instance FromJSON a => FromJSON (M.HashMap Channel a) where
    parseJSON v =
        let f (c, x) = (Channel c, x)
        in  M.fromList . map f . M.toList <$> parseJSON v

instance ToJSON a => ToJSON (M.HashMap Channel a) where
    toJSON m =
        let f (Channel c, x) = (c, x)
        in  toJSON $ M.fromList $ map f $ M.toList m

instance FromJSON ChanInfo where
    parseJSON (Object o) =
        ChanInfo <$>
        o .: "track" <*>
        o .: "count" <*>
        o .: "log" <*>
        o .: "history-lines" <*>
        o .: "default-response"
    parseJSON _          = mzero

instance ToJSON ChanInfo where
    toJSON (ChanInfo tracking counting logging hls defresp) = object
        [ "track"            .= tracking
        , "count"            .= counting
        , "log"              .= logging
        , "history-lines"    .= hls
        , "default-response" .= defresp
        ]

instance FromJSON BotStateJ where
    parseJSON (Object o) =
        BotStateJ <$>
        o .: "chan-state" <*>
        o .: "chans-join"
    parseJSON _          = mzero

instance ToJSON BotStateJ where
    toJSON (BotStateJ chans sel) = object
        [ "chan-state" .= chans
        , "chans-join" .= sel
        ]

instance ToJSON (BotState s) where
    toJSON bstate = toJSON $ toJ bstate

loadBotState :: BotEnv e s -> s -> IO (BotState s)
loadBotState env pub = do
    let conf = beConfig env
    r <- loadState $ stateFilePath (cfgStateFile conf) (cfgStateRepo conf)
    case r of
        Left (False, e) -> error $ "Failed to read state file: " ++ e
        Left (True, e)  -> error $ "Failed to parse state file: " ++ e
        Right sj        -> fromJ env sj pub

mkSaveBotState :: Config -> IO (BotState s -> IO ())
mkSaveBotState conf =
    let iv = fromInteger $ microseconds $ cfgSaveInterval conf :: Microsecond
        msg = "auto commit by irc-fun-bot"
    in  mkSaveStateChoose iv (cfgStateFile conf) (cfgStateRepo conf) msg

saveBotState :: Session e s ()
saveBotState = do
    bstate <- get
    save <- asks beSaveState
    liftIO $ save bstate

-- | Add a channel to the persistent list of channels to be joined. Next time
-- the bot launches (or, say, 'joinConfig` is called), it will join this
-- channel. If the channel is already listed, nothing happens.
selectChannel :: Channel -> Session e s ()
selectChannel chan = do
    chans <- gets bsSelChans
    unless (chan `S.member` chans) $ do
        modify $ \ s -> s { bsSelChans = S.insert chan chans }
        saveBotState

-- | Remove a channel from the persistent list of channels to be joined. Next
-- time the bot launches, it won't join this channel (unless listed in the
-- config or otherwise requested). If the channel isn't listed, nothing
-- happens.
unselectChannel :: Channel -> Session e s ()
unselectChannel chan = do
    chans <- gets bsSelChans
    when (chan `S.member` chans) $ do
        modify $ \ s -> s { bsSelChans = S.delete chan chans }
        saveBotState

-- | Add default channel state for the given channel. It will be stored into
-- the state file. If the channel already has state, nothing will happen.
addChannelState :: Channel -> Session e s ()
addChannelState chan = do
    chans <- getChans
    unless (chan `M.member` chans) $ do
        putChans $ M.insert chan (ChanState False False Nothing 0 True) chans
        saveBotState
